// locutor
// (c) 2008-2009, Normen Müller, normen.mueller@gmail.com  
// $Id: Registry.scala 2479 2009-10-06 11:17:29Z nmueller $
package locutor.registry

import java.io.File
import java.net.URI
import java.text.MessageFormat.{format}

import scaup.Views._
import scaup.io.FileType._

import locutor.Views._
import locutor.svn._
import locutor.svn.SVN.{externalsOf,unlock}

case class RegistryWarning(message: String) extends Throwable(message)
case class RegistryError(message: String) extends Throwable(message)

/** The companion object <code>Registry</code> provides some Registry constants.
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
object Registry {  
  val version = "0.1"
  
  val namespace = "http://locutor.kwarc.info"  
}

/** The class <code>Registry</code> implements a registry for Subversion working copies.
 * 
 *  <p>A Subversion working copy is an ordinary but version controlled directory tree on your local system,
 *  containing a collection of files.</p>
 * 
 *  <p><strong>Note:</strong> No parameter validation is performed.  Caller is responsible for passing valid
 *  arguments.</p>
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
class Registry @throws(classOf[RegistryError]) (file: File) {
  import Decoder._
  import Encoder._
  import Tools._
  
  private var ws = decode(file)
  assert(dereferencable,"\n[scaup|registry]: Assertion ``Dereferencable'' failed!\n"+ws+"\n")
  
  @throws(classOf[RegistryError]) @throws(classOf[RegistryWarning])
  def insert(wr: File): Unit = wr.vc.url match {
    case Some(wu) =>
      if(ws \! {case WC(u,d,_) if d == wr && u != wu => true})
        throw RegistryError(format("At ''{0}'' another working copy is already registered.",wr))
      else if (ws \! {case WC(u,d,_) if d == wr => true})
        throw RegistryWarning(format("Working copy at ''{0}'' already registered.",wr))
      else ws = WC(wu,wr,decode(externalsOf(wr))) :: ws
      assert(dereferencable,"\n[scaup|registry]: Assertion ``Dereferencable'' failed!\n"+ws+"\n")
    case None => throw RegistryWarning(format("''{0}'' is not versioned.",wr))    
  }
  
  @throws(classOf[RegistryError]) @throws(classOf[RegistryWarning])  
  def update(wr: File,own: File,es: java.util.List[String]): Unit = wr.vc.url match {
    case Some(wu) =>
      if(ws \! {case WC(u,d,_) if d == wr && u != wu => true})
        throw RegistryError(format("At ''{0}'' another working copy is already registered.",wr))
      else (ws \? {case WC(u,d,_) if d == wr => true}) match {
        case Some(w) =>         
          import scaup.data.Conversions._
          import locutor.svn.EXTDEF.{parse}
          ws = adjust(ws,w,extsOf(w) ::: texsOf(w) filter (_.own == own),
                      decode(own,(es.elements filterMapOption parse).toList))
          assert(dereferencable,"\n[scaup|registry]: Assertion ``Dereferencable'' failed!\n"+ws+"\n")
          assert(simplified,"\n[scaup|registry]: Assertion ``Simplified'' failed!\n"+ws+"\n")
        case None => throw RegistryWarning(format("Working copy at ''{0}'' not registered.",wr))
      }
    case None => throw RegistryWarning(format("''{0}'' is not versioned.",wr))    
  }
  
  @throws(classOf[RegistryWarning])
  def delete(wr: File): Unit = ws \? {case WC(u,d,_) if d == wr => true} match {
    case Some(w) =>
      ws = ((vs: List[VC]) => transform(vs,resolve(vs,depsOf(vs,w),Map.empty[VC,VC])))(remove(reset(ws,ws,texsOf(w)),w))
      assert(dereferencable,"\n[scaup|registry]: Assertion ``Dereferencable'' failed!\n"+ws+"\n")
      assert(simplified,"\n[scaup|registry]: Assertion ``Simplified'' failed!\n"+ws+"\n")    
    case None => throw RegistryWarning(format("Working copy at ''{0}'' not registered.",wr))
  }
  
  @throws(classOf[RegistryError]) @throws(classOf[RegistryWarning])
  def sanitize(wr: File): Unit = wr.vc.url match {
    case Some(wu) =>
      if(ws \! {case WC(u,d,_) if d == wr && u != wu => true})
        throw RegistryError(format("At ''{0}'' another working copy is already registered.",wr))
      else (ws \? {case WC(u,d,_) if d == wr => true}) match {
        case Some(w) =>
          ws = adjust(ws,w,extsOf(w) ::: texsOf(w),decode(externalsOf(wr)))
//          ws = transform(ws,resolve(ws,w,Map.empty[VC,VC]))
          assert(dereferencable,"\n[scaup|registry]: Assertion ``Dereferencable'' failed!\n"+ws+"\n")
          assert(simplified,"\n[scaup|registry]: Assertion ``Simplified'' failed!\n"+ws+"\n")
        case None => throw RegistryWarning(format("Working copy at ''{0}'' not registered.",wr))
      }
    case None => throw RegistryWarning(format("''{0}'' is not versioned.",wr))    
  }
  
  def serialize() = xml.XML.saveFull(file.ap,encode(ws),"UTF-8",true,null)
  
  private def dereferencable = (ws flatMap texsOf) forall (t => flatten(ws) exists (_.id == t.ref))
  
  private def simplified = (ws flatMap texsOf) forall (t => flatten(ws) find (_.id == t.ref) match {
    case Some(d) =>
      (
           (d.isInstanceOf[WC] || d.isInstanceOf[EXT])
        && (if(t.dir exists)
              t.dir.t == SYMLINK && t.dir.cf.t == DIRECTORY
            else true)
      )
    case _ => false
  })
}

/**
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
private [registry] object Decoder {
  import Registry._
  import scala.xml.{NodeSeq,Node}
  
  implicit private def n2s(n: NodeSeq): String = n.text
  implicit private def n2u(n: NodeSeq): URI = n.text.toURI
  implicit private def n2f(n: NodeSeq): File = n.text.toFile
  implicit private def n2l(n: NodeSeq): Long = n.text.toLong

  @throws(classOf[RegistryError])
  def decode(file: File): List[VC] = {
    import org.xml.sax.{SAXParseException => SAXPE}
    if(file == null)
      throw RegistryError(format("Physical registry file may not be null."))
    else if(file.exists && file.isDirectory)
      throw RegistryError(format("Physical registry file may not be a directory."))
    else if(file exists)
      decode(
        try {
          xml.XML.loadFile(file)
        } catch {
          case ex: SAXPE => throw RegistryError(format("Registry XML parsing error:\n''{0}''",ex.getMessage))
        })
    else Nil
  }
  
  private def decode(n: Node): List[VC] = scala.xml.Utility.trim(n) match {
    case r@(<registry>{es@_*}</registry>) if r \ "@version" != version =>
      throw RegistryError("Invalid registry version.")
    case r@(<registry>{es@_*}</registry>) if r.namespace != namespace =>
      throw RegistryError("Invalid registry namespace.")
    case r@(<registry>{es@_*}</registry>) => translate(es)    
  }
  
  private def translate(n: Node): VC = n match {
    case w@(<wc>{es@_*}</wc>) =>
      WC(w \ "@url",w \ "@root",translate(es))
    case e@(<external>{es@_*}</external>) if (e \ "@xref").text isEmpty =>
      EXT(e \ "@own",e \ "@rev",e \ "@url",e \ "@tgt",translate(es))
    case t@(<external/>) =>
      TEX(t \ "@own",t \ "@rev",t \ "@url",(t \ "@tgt").text,(t \ "@xref").substring(1))
  }
  
  private def translate(ns: Seq[Node]): List[VC] = ns match {
    case Seq(hl,tl@_*) => translate(hl) :: translate(tl)
    case _             => Nil
  }

  def decode(e: EXT): VC with Definitional = ((f: File) =>
    if(f.t == SYMLINK)
      TEX(e.own,e.rev,e.uri,e.tgt,f.lnktgt.get.toMD5)
    else EXT(e.own,e.rev,e.uri,e.tgt,decode(externalsOf(f))))(e.own/e.tgt)
  
  def decode(es: List[EXT]): List[VC with Definitional] = es match {
    case hl::tl => decode(hl) :: decode(tl)
    case _      => Nil
  }
  
  def decode(own: File,e: ExtDef): VC with Definitional =
    EXT(own,e.rev,e.resolve(own.vc.repos.get,own.vc.url.get),e.tgt,decode(externalsOf(own/e.tgt)))
  
  def decode(own: File,es: List[ExtDef]): List[VC with Definitional] = es match {
    case hl::tl => decode(own,hl) :: decode(own,tl)
    case _ => Nil
  }
}

/**
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
private [registry] object Encoder {
  import Registry._
  
  def encode(l: List[VC]): scala.xml.Node = (<registry xmlns={namespace} version={version}>{l map (_.toXML)}</registry>)
}

/**
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
private [registry] object Tools {
  import Decoder._
  
  def resolve(vs: List[VC],v: VC,m: Map[VC,VC]): Map[VC,VC] = ((vs: List[VC]) => v match {
    case w@WC(u,d,es)      => resolve(vs,es,m)
    case e@EXT(o,r,u,t,es) => findRedundant(vs,e) match {
      case Some(r) => 
        ((im: List[VC]) =>
          resolve(remove(vs,e),
                  ((e :: extsOf(es)) flatMap (depsOf(vs,_))) union im,
                  (m -- im) + {(e,r)})
        )((m.keys filter (k => ((e :: extsOf(e)) exists (_.id == m(k).id)))).toList)
      case None    => resolve(vs,es,m)
    }
    case e@TEX(o,r,u,t,x) => m.keys find (_.id == x) match {
      case Some(r) => m + {(e,m(r))}
      case None    => drefOf(vs,e) match {
        case Some(r@(_:WC|_:EXT)) if r ~> e => m
        case _                              => findRedundant(vs,e) match {
          case Some(r) => m + {(e,r)}
          case None    => m
        }
      }
    }
  })(remove(vs,m.keys.toList))
  
  def resolve(vs: List[VC],es: List[VC],m: Map[VC,VC]): Map[VC,VC] = es match {
    case hl::tl => resolve(vs,tl,resolve(vs,hl,m))
    case _      => m
  }
  
//  // WC prioritization; pre-condition: flt is flattened; post-condition: r ~ e
//  private def findRedundant(v: VC,e:VC): Option[VC] = v match {case r@(_:WC | _:EXT) if r ~ e => Some(r) case _ => None}
//  private def findRedundant(flt: List[VC],e: VC): Option[VC] = flt match {
//    case hl::tl => findRedundant(hl,e) match {
//      case None => findRedundant(tl,e)
//      case r@_ => r
//    }
//    case _ => None    
//  }
  // First match; post-condition: r ~ e && r.dir exists
  private def findRedundant(v: VC,e:VC): Option[VC] = v match {
    case r@WC(_,_,es) if r ~> e && (r.dir exists)      => Some(r)
    case r@WC(_,_,es)                                  => findRedundant(es,e)
    case r@EXT(_,_,_,_,es) if r ~> e && (r.dir exists) => Some(r)
    case r@EXT(_,_,_,_,es)                             => findRedundant(es,e)
    case _                                             => None
  }
  
  private def findRedundant(vs: List[VC],e: VC): Option[VC] = vs match {
    case hl::tl => findRedundant(hl,e) match {
      case None => findRedundant(tl,e)
      case r@_  => r
    }
    case _      => None    
  }
  
  def remove(vs: List[VC],v: VC): List[VC] = vs match {
    case hl::tl => hl match {
      case z@WC(u,d,es) if z.id == v.id      => remove(tl,v)
      case WC(u,d,es)                        => WC(u,d,remove(es,v)) :: remove(tl,v)
      case z@EXT(o,r,u,t,es) if z.id == v.id => remove(tl,v)
      case EXT(o,r,u,t,es)                   => EXT(o,r,u,t,remove(es,v)) :: remove(tl,v)
      case z@TEX(o,r,u,t,x) if z.id == v.id  => remove(tl,v)
      case TEX(o,r,u,t,x)                    => TEX(o,r,u,t,x) :: remove(tl,v)
    }
    case _      => Nil
  }
  
  def remove(vs: List[VC],rs: List[VC]): List[VC] = rs match {
    case hl::tl => remove(remove(vs,hl),tl)
    case _      => vs
  }
    
  def adapt(cs: List[VC],v: VC,as: Map[String,VC with Definitional]): VC = v match {
    case WC(u,d,es)                             => WC(u,d,adapt(cs,es,as))
    case e@EXT(o,r,u,t,es) if as contains e.id  =>
      ((x: EXT) =>
        transform(remove(cs,e),x,resolve(remove(cs,e),x,Map.empty[VC,VC]))
      )(EXT(o,as(e.id).rev,as(e.id).uri,t,decode(externalsOf(e.dir))))
    case EXT(o,r,u,t,es)                        => EXT(o,r,u,t,adapt(cs,es,as))
    case t@TEX(o,r,u,tgt,x) if as contains t.id =>
      if(t.dir.t == SYMLINK)
        ((e: EXT) => {
          t.dir.remove()
          transform(remove(cs,t),e,resolve(remove(cs,t),e,Map.empty[VC,VC]))
        })(EXT(o,as(t.id).rev,as(t.id).uri,tgt,Nil))
      else
        ((e: EXT) =>
          transform(remove(cs,t),e,resolve(remove(cs,t),e,Map.empty[VC,VC]))
        )(EXT(o,as(t.id).rev,as(t.id).uri,tgt,decode(externalsOf(t.dir))))      
    case TEX(o,r,u,t,x)                         => TEX(o,r,u,t,x)
  }
  
  def adapt(cs: List[VC],es: List[VC],as: Map[String,VC with Definitional]): List[VC] = es match {
    case hl::tl => adapt(cs,hl,as) :: adapt(cs,tl,as)
    case _      => Nil
  }
  
  def append(v: VC,at: VC,is: List[VC with Definitional]): VC = v match {
    case w@WC(u,d,es) if w.id == at.id      => WC(u,d,es ::: is)
    case WC(u,d,es)                         => WC(u,d,append(es,at,is))
    case e@EXT(o,r,u,t,es) if e.id == at.id => EXT(o,r,u,t,es ::: is)
    case EXT(o,r,u,t,es)                    => EXT(o,r,u,t,append(es,at,is))
    case TEX(o,r,u,t,x)                     => TEX(o,r,u,t,x)
  }
  
  def append(vs: List[VC],at: VC,is: List[VC with Definitional]): List[VC] = vs match {
    case hl::tl => append(hl,at,is) :: append(tl,at,is)
    case _      => Nil
  }
  
  def adjust(ws: List[VC],v: VC,rs: List[VC with Definitional],cs: List[VC with Definitional]): List[VC] = {
    val toRemove = rs filter (r => !(cs exists (_.dir == r.dir)))
    val toAdapt = (Map.empty[VC,VC with Definitional] /: (rs flatMap ((r: VC with Definitional) =>
      cs find (c => c.dir == r.dir && ((c.rev != r.rev) || (c.uri != r.uri))) match {
        case Some(c) => List((r,c))
        case _       => Nil
      })))(_+_)            
    val toAppend = cs filter (c => !(rs exists (_.dir == c.dir)))
    val toReplace = rs filter (r => cs exists (c => c.dir == r.dir && c.rev == r.rev && c.uri == r.uri)) map {
      case e@EXT(o,r,u,t,es) => EXT(o,r,u,t,decode(externalsOf(e.dir)))
      case z@_               => z
    }    
            
    ((ws: List[VC]) =>
      ((vs: List[VC]) => 
        transform(vs,resolve(vs,toReplace,Map.empty[VC,VC]))
      )(replace(ws,v,toReplace))
    )(((ws: List[VC]) => 
      ((vs: List[VC]) =>
        transform(vs,resolve(vs,toAppend,Map.empty[VC,VC]))
      )(append(ws,v,toAppend))
    )(((ws: List[VC]) => 
      ((vs: List[VC]) =>
        transform(vs,resolve(vs,
                             toAdapt.keys.toList flatMap (e => e :: extsOf(e) flatMap (depsOf(vs,_))),
                             Map.empty[VC,VC]))
      )(adapt(ws,ws,Map((toAdapt map (kv => (kv._1.id,kv._2))).toSeq:_*)))
    )(((vs: List[VC]) =>
      transform(vs,resolve(vs,toRemove flatMap (e => e :: extsOf(e) flatMap (depsOf(vs,_))),Map.empty[VC,VC]))
    )(remove(ws,toRemove)))))    
  }
  
  private def replace(v: VC,at: VC, rs: List[VC]): VC = v match {
    case w@WC(u,d,es) if w.id == at.id      => WC(u,d,remove(es,rs) ::: rs)
    case WC(u,d,es)                         => WC(u,d,replace(es,at,rs))
    case e@EXT(o,r,u,t,es) if e.id == at.id => EXT(o,r,u,t,remove(es,rs) ::: rs)
    case EXT(o,r,u,t,es)                    => EXT(o,r,u,t,replace(es,at,rs))
    case TEX(o,r,u,t,x)                     => TEX(o,r,u,t,x)
  }
  
  private def replace(vs: List[VC],at: VC, rs: List[VC]): List[VC] = vs match {
    case hl::tl => replace(hl,at,rs) :: replace(tl,at,rs)
    case _      => Nil
  }

  private def reset(cs: List[VC],vs: List[VC],t: TEX): List[VC] = vs match {
    case hl::tl => hl match {
      case WC(u,d,es)                         => WC(u,d,reset(cs,es,t)) :: reset(cs,tl,t)
      case EXT(o,r,u,tgt,es)                  => EXT(o,r,u,tgt,reset(cs,es,t)) :: reset(cs,tl,t)
      case z@TEX(o,r,u,tgt,x) if z.id == t.id => 
        t.dir.remove()
        drefOf(cs,t) match {
          case Some(d) => unlock(d.dir/(u -- d.uri) copyTo t.dir)
          case None    => ()
        }
        EXT(o,r,u,tgt,decode(externalsOf(t.dir))) :: reset(cs,tl,t)
      case TEX(o,r,u,tgt,x)                   => TEX(o,r,u,tgt,x) :: reset(cs,tl,t)
    }
    case _      => Nil
  }
  
  def reset(cs: List[VC],vs: List[VC],ts: List[TEX]): List[VC] = ts match {
    case hl::tl => reset(cs,reset(cs,vs,hl),tl)
    case _      => vs
  }  
  
  def transform(vs: List[VC],v: VC,ts: Map[VC,VC]): VC = v match {
    case WC(u,d,es)                         => WC(u,d,transform(vs,es,ts))
    case e@EXT(o,r,u,t,es) if ts contains e => 
      ((f: File) =>
        if(f exists) {
          e.dir linkTo f.p
          TEX(o,r,u,t,ts(e).id)
        } else EXT(o,r,u,t,es)
      )(ts(e).dir/(u -- ts(e).uri))
    case EXT(o,r,u,t,es)                    => EXT(o,r,u,t,transform(vs,es,ts))
    case e@TEX(o,r,u,t,x) if ts contains e  =>
      ((f: File) =>
        if(f exists) {
          e.dir linkTo f.p
          TEX(o,r,u,t,ts(e).id)
        } else {
          e.dir.remove()
          EXT(o,r,u,t,Nil)
        }
      )(ts(e).dir/(u -- ts(e).uri))
    case e@TEX(o,r,u,t,x)                   => drefOf(vs,e) match {
      case Some(d) if d ~> e && (d.dir exists) =>
        e.dir.remove()
        e.dir linkTo (d.dir/(u -- d.uri)).p
        TEX(o,r,u,t,x)
      case Some(d)                             =>
        e.dir.remove()
        ((e: EXT) =>
          transform(remove(vs,d),e,resolve(remove(vs,d),e,ts))
        )(EXT(o,r,u,t,Nil))
      case None =>
        unlock(e.dir.cf copyTo e.dir.remove()) // critical: e.dir.cf might no longer be adequate
        ((e: EXT) => 
          transform(vs,e,resolve(vs,e,ts))
        )(EXT(o,r,u,t,decode(externalsOf(e.dir))))
    }
  }
  
  def transform(vs: List[VC],ts: Map[VC,VC]): List[VC] = transform(vs,vs,ts)
  
  private def transform(vs: List[VC],es: List[VC],ts: Map[VC,VC]): List[VC] = es match {
    case hd::tl => transform(vs,hd,ts) :: transform(vs,tl,ts)
    case _      => Nil
  }
  
  def extOf(v: VC): List[EXT] = ((Nil: List[EXT]) /: (v.es filterMap {case e:EXT => e}))((l,e) => e :: l)
  
  def extsOf(v: VC): List[EXT] = extOf(v) ::: extsOf(v.es)
  
  private def extsOf(vs: List[VC]): List[EXT] = vs match {case hl::tl => extsOf(hl) ::: extsOf(tl) case _ => Nil}

  def texOf(v: VC): List[TEX] = ((Nil: List[TEX]) /: (v.es filterMap {case e:TEX => e}))((l,e) => e :: l)
  
  def texsOf(v: VC): List[TEX] = texOf(v) ::: texsOf(v.es)
  
  private def texsOf(vs: List[VC]): List[TEX] = vs match {case hl::tl => texsOf(hl) ::: texsOf(tl) case _ => Nil}
  
  def depsOf(vs: List[VC],v: VC): List[VC] = texsOf(vs) filter (_.ref == v.id)
  
  def drefOf(vs: List[VC],t: TEX): Option[VC] = vs ::: extsOf(vs) find (_.id == t.ref)
  
  def flatten(vs: List[VC]): List[VC] = vs ::: extsOf(vs)
}
